package com.leo.train.business.service.Impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.leo.train.business.dto.SkTokenDto;
import com.leo.train.business.entity.SkToken;
import com.leo.train.business.mapper.DailyTrainInfoMapper;
import com.leo.train.business.mapper.SkTokenMapper;
import com.leo.train.business.mapper.cust.SkTokenMapperCust;
import com.leo.train.business.service.DailySeatService;
import com.leo.train.business.service.SkTokenService;
import com.leo.train.common.enums.RedisKeyPreEnum;
import com.leo.train.common.exception.BusinessException;
import com.leo.train.common.exception.BusinessExceptionEnum;
import com.leo.train.common.resp.PageResult;
import com.leo.train.common.util.SnowUtil;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class SkTokenServiceImpl implements SkTokenService {
    @Autowired
    private SkTokenMapper skTokenMapper;
    @Autowired
    private DailySeatService dailySeatService;
    @Autowired
    private DailyTrainInfoMapper dailyTrainInfoMapper;
    @Autowired
    private SkTokenMapperCust skTokenMapperCust;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private RedisTemplate redisTemplate;
    @Value("${spring.profiles.active}")
    private String env; // 获取环境变量
    @Override
    public PageResult<SkToken> list(SkTokenDto skTokenDto) {
        PageHelper.startPage(skTokenDto.getPage(), skTokenDto.getPageSize());
        Page<SkToken> pageResult = skTokenMapper.list(skTokenDto);
        return new PageResult<>(pageResult.getTotal(), pageResult.getResult());
    }

    @Override
    public void update(SkToken skToken) {
        skToken.setUpdateTime(new Date());
        skTokenMapper.update(skToken);
    }

    @Override
    public SkToken detail(Long id) {
        return skTokenMapper.selectByPrimaryKey(id);
    }

    /**
     * 生成指定日期的指定车次的秒杀令牌
     * @param date
     * @param trainCode
     */
    @Override
    public void genDaily(Date date, String trainCode) {
        // 1.删除指定日期指定车次的秒杀令牌
        skTokenMapper.deleteDaily(date, trainCode);
        // 2. 生成指定日期的指定车次的秒杀令牌
        Date now = new Date();
        SkToken skToken = new SkToken();
        skToken.setId(SnowUtil.getSnowflakeNextId());
        skToken.setDate(date);
        skToken.setTrainCode(trainCode);
        skToken.setCreateTime(now);
        skToken.setUpdateTime(now);

        // 计算出秒杀令牌的数量
        // 计算出一趟车的座位数量
        int seatCount = dailySeatService.countSeat(date, trainCode, "");
        // 计算出这趟车有多少站
        int stationCount = dailyTrainInfoMapper.countByTrainCodeAndDate(trainCode, date);
        // 最大座位数乘以站点数，就是秒杀令牌数量
        int count = seatCount * stationCount;
        // 最大秒杀令牌数量 * 3/4，就是大概的秒杀令牌数量
        count = count * 3 / 4;
        skToken.setCount(count);
        skTokenMapper.insert(skToken);

    }

    /**
     * 验证指定日期的指定车次是否有秒杀令牌余量
     * @param date
     * @param trainCode
     * @return
     */
    @Override
    public boolean validSkToken(Date date, String trainCode,Long memberId) {
        // 1. 为了防止机器人刷票，使用分布式锁，加锁(防重复提交锁)
        String lockKey = RedisKeyPreEnum.SK_TOKEN  + "-" +  date + trainCode + "-" + memberId;
        RLock lock = null;
        try {
            // 使用redisson，自带看门狗
            lock = redissonClient.getLock(lockKey);
            // 尝试获取锁，如果获取不到说明用户刚刚提交过，直接返回false防止重复提交
            // 锁续时间设置为5秒，防止用户短时间内重复提交
            boolean tryLock = lock.tryLock(0, 5, TimeUnit.SECONDS);
            if (!tryLock) {
                log.info("获取防重复提交锁失败：{}", lockKey);
                return false;
            }

            log.info("获取防重复提交锁成功：{}", lockKey);
            // 2. 加入redis缓存 --- 将秒杀令牌数量加入redis缓存，只有超过5次才调用数据库更新，防止大量的数据库更新
            String skTokenCountKey = RedisKeyPreEnum.SK_TOKEN_COUNT + "-" + date + trainCode;
            Object obj = redisTemplate.opsForValue().get(skTokenCountKey);
            if (obj != null) {
                Long count = redisTemplate.opsForValue().decrement(skTokenCountKey,1);
                if(count < 0L){
                    return false;
                } else {
                    log.info("获取令牌数成功，剩余令牌数：{}", count);
                    // 获取到缓存的key后，将缓存key重置为60秒，防止用户频繁请求数据库
                    redisTemplate.expire(skTokenCountKey, 60, TimeUnit.SECONDS);
                    // 每获取5个令牌则更新一次数据库
                    if(count % 5 == 0) {
                        skTokenMapperCust.decrease(date, trainCode,5);
                    }
                    return true;
                }
            } else {
                // 不存在key，添加redis缓存
                // 3. 查询指定日期指定车次的秒杀令牌
                int skTokenCount  = skTokenMapper.selectByTrainCodeAndDate(date, trainCode);
                if(skTokenCount > 0){
                    Long count = Long.valueOf(skTokenCount - 1);
                    // 存的时候，将余量确保是数字类型
                    redisTemplate.opsForValue().set(skTokenCountKey, count, 60, TimeUnit.SECONDS);
                    return  true;
                } else {
                    // 没有库存，立即释放锁，允许用户查询其他车次
                    if (lock != null && lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                    return false;
                }

            }
        } catch (InterruptedException e) {
            // 发生异常，确保锁被释放
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
            throw new BusinessException(BusinessExceptionEnum.CONFIRM_ORDER_EXCEPTION);
        }
    }
}
